using System;
using System.Collections.Generic;
using ObjectSync.Interfaces;
using System.ComponentModel;
using System.Runtime.Serialization.Formatters.Binary;
using Lidgren.Network;
using System.IO;
using System.Threading;

namespace ObjectSync
{
    struct NetObjects
    {
        public int ID;
        public ISyncable Obj;
    }

    //public struct TransmissionData
    //{
    //    public int oid;
    //    public List<object> PropertiesValues;
    //}
    /// <summary>
    /// Manager to make the sync works. Hold the main functions
    /// </summary>
    public sealed class SyncManager
    {

		/*
		private NetOutgoingMessage buffMessage;

		public NetOutgoingMessage BuffMessage {
			get {
				return buffMessage;
			}
			set {
				buffMessage = value;
			}
		}
		*/



        // TODO: Study the need of mutex here 

       // The thread for processing delegates.
        private BackgroundWorker workThread;

        private Object lockObject = new Object();

        public delegate void ApplyDelegate(SyncData data); 

        // Inidicates whether the delegate queue has been disposed.
        private volatile bool disposed = false;

        private List<NetObjects> BaseObjects = new List<NetObjects>();
        
        private List<NetObjects> RefObjects = new List<NetObjects>();

        private static bool firstRun = true;
        
        /// <summary>
        /// Get the next unused netId number
        /// </summary>
        public int NextNetID
        {
            get
            {
                return IDManager.Instance.NextNetID;
            }
        }

        #region SINGLETON
        static readonly SyncManager instance = new SyncManager();

        // Explicit static constructor to tell C# compiler
        // not to mark type as beforefieldinit
        static SyncManager()
        {
            
        }

        SyncManager()
        {
        }

        public static SyncManager Instance
        {
            get
            {
                if (firstRun)
                {
                    firstRun = false;
                    instance.InitializeWorker();
                }
                return instance;
            }
        }
        #endregion


        private void InitializeWorker()
        {
            workThread = new BackgroundWorker();

            workThread.DoWork += new DoWorkEventHandler(workThread_DoWork);
            workThread.RunWorkerCompleted += new RunWorkerCompletedEventHandler(workThread_RunWorkerCompleted);
            //workThread.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
        }

        // This event handler to be shure the data is unique
        private void workThread_DoWork(object sender,
            DoWorkEventArgs e)
        {
            // Get the BackgroundWorker that raised this event.
            //BackgroundWorker worker = sender as BackgroundWorker;
            lock (lockObject)
            {
                AplyChangesToObjectsInternal((SyncData)e.Argument);
            }
        }


        // This event handler deals with the results of the
        // background operation.
        private void workThread_RunWorkerCompleted(
            object sender, RunWorkerCompletedEventArgs e)
        {
            // First, handle the case where an exception was thrown.
            if (e.Error != null)
            {
                // TODO: Place error management here (LOG?)
            }
            else if (e.Cancelled)
            {
                // We won´t cancell
            }
            else
            {
                // Finally, handle the case where the operation 
                // succeeded.
            }
        }
        
        #region OBJECT HANDLING

        /// <summary>
        /// Register an object to be synced
        /// </summary>
        /// <param name="o">ISyncable object</param>
        public void Register(ref SyncableObject o)
        {
            ISyncable iobj = o;
            Register(ref iobj);
            o = (SyncableObject)iobj;
        }
        /// <summary>
        /// Register an object to be synced
        /// </summary>
        /// <param name="o">ISyncable object</param>
        public void Register(ref ISyncable o)
        {
            if (o == null) throw  new Exception("Impossible to register null objects");
            int id = o.NetId;
            if (BaseObjects.Find(s => s.ID == id).Obj != null) throw new Exception("This object is already registred or there is another one with the same NetId");

            var No = new NetObjects();
            No.ID = o.NetId;
            No.Obj = o;
            RefObjects.Add(No);
            var Nob = new NetObjects();
            Nob.ID = o.NetId;
            Nob.Obj = o.Copy();
            BaseObjects.Add(Nob);
        }

        /// <summary>
        /// returns an object alread registred 
        /// </summary>
        /// <param name="?"></param>
        public ISyncable getRegistred(int id)
        {
            lock (lockObject)
            {
                workThread.DoWork += new DoWorkEventHandler(registred_DoWork);
                workThread.RunWorkerCompleted  += new RunWorkerCompletedEventHandler(registred_RunWorkerCompleted);
                workThread.RunWorkerAsync(id);
                
                while (workThread.IsBusy)
                {
                    Thread.Sleep(2);

                }

                workThread.DoWork += new DoWorkEventHandler(workThread_DoWork);
                workThread.RunWorkerCompleted += new RunWorkerCompletedEventHandler(workThread_RunWorkerCompleted);
                
                return returnSyncObj;
            }
        }

        public void applyChangeToRelatedObject(ISyncable obj )
        {
            SyncData data = new SyncData();
            data.PropertiesNames = new List<string>();
            data.PropertiesValues = new List<object>();

            data.oid = obj.NetId;

            foreach (var dt in obj.SyncableProps)
            {
                data.PropertiesNames.Add(obj.GetType().GetProperty(dt).Name);
                data.PropertiesValues.Add(obj.GetType().GetProperty(dt).GetValue(obj, null));
            }
            AplyChangesToObjects(data, true);
        }


        private void registred_DoWork(object sender,
            DoWorkEventArgs e)
        {
            int id = (int)e.Argument;
            e.Result = getRegistredInternal(id);

        }

        private ISyncable returnSyncObj;

        private void registred_RunWorkerCompleted(
            object sender, RunWorkerCompletedEventArgs e)
        {
            returnSyncObj = (ISyncable)e.Result;
        }

        /// <summary>
        /// returns an object alread registred 
        /// </summary>
        /// <param name="?"></param>
        private ISyncable getRegistredInternal(int id)
        {
            NetObjects bobj = RefObjects.Find(s => s.ID == id);
            return bobj.Obj;
        }

        #endregion 

        #region SYNC
        /// <summary>
        /// Do the actual Syncronization
        /// </summary>
		public void Syncronize()
        {

            //string message = "SYNC$";

            NetBuffer BuffMessage = new NetBuffer();

            BuffMessage.Write((UInt16)MessageType.SYNC);

            List<SyncData> TList = new List<SyncData>();
            
            foreach (var o in RefObjects)
            {
                int id = o.ID;
                NetObjects bobj = BaseObjects.Find(s => s.ID == id);

                Boolean first = true;

                SyncData transData = new SyncData { 
                    PropertiesValues = new List<object>(),
                    PropertiesNames = new List<String>()};

                // int i = 0;
                foreach (var propName in bobj.Obj.SyncableProps)
                {
                    Object valBase = bobj.Obj.GetType().GetProperty(propName).GetValue(bobj.Obj, null);
                    Object valSync = o.Obj.GetType().GetProperty(propName).GetValue(o.Obj, null);

                    
                    if (valBase != valSync)
                    {
                        if (first)
                        {
                            transData.oid = bobj.ID;
                            first = false;
                        }
                        
                        bobj.Obj.GetType().GetProperty(propName).SetValue(bobj.Obj, valSync, null);

                        transData.PropertiesNames.Add(propName);
                        transData.PropertiesValues.Add(valSync);
                    }
                }

                TList.Add(transData);
            }

            // Writting the number of objects we are syncing 
            BuffMessage.Write(TList.Count);

            foreach (var TData in TList)
            {
                mountBuffMessageFromSyncData(ref BuffMessage, TData);
            }

            //Sending the sincronization Message
            NetworkManager.Instance.Communicator.Send(BuffMessage);
            
        }

        private void mountBuffMessageFromSyncData(ref NetBuffer BuffMessage, SyncData TData)
        {
            BuffMessage.Write(TData.oid);
            BuffMessage.Write(TData.PropertiesValues.Count);
            int i = 0;
            foreach (var obj in TData.PropertiesValues)
            {
                BinaryFormatter formatter = new BinaryFormatter();
                using (Stream str = new MemoryStream())
                {
                    formatter.Serialize(str, obj);
                    str.Position = 0;
                    BuffMessage.Write(TData.PropertiesNames[i]);
                    i++;
                    BuffMessage.Write(str.Length);

                    Byte[] outBuff = new Byte[str.Length];
                    str.Read(outBuff, 0, (int)str.Length);

                    BuffMessage.Write(outBuff);
                }
            }
        }

        /// <summary>
        /// Changes the objects based on info given by the SyncData
        /// </summary>
        /// <param name="data"></param>
        public void AplyChangesToObjects(SyncData data, bool server)
        {
            if (server)
            {
                NetBuffer BuffMessage = new NetBuffer();
                BuffMessage.Write((UInt16)MessageType.SYNC);
                BuffMessage.Write(1);
                mountBuffMessageFromSyncData(ref BuffMessage, data);
                NetworkManager.Instance.Communicator.Send(BuffMessage);
            }
            AplyChangesToObjects(data);
        }

        /// <summary>
        /// Changes the objects based on info given by the SyncData
        /// </summary>
        /// <param name="data"></param>
        public void AplyChangesToObjects(SyncData data)
        {
            if(!workThread.IsBusy)
                workThread.RunWorkerAsync(data);
            while (workThread.IsBusy)
            {
                Thread.Sleep(3);
            }
        }

        /// <summary>
        /// Changes the objects based on info given by the SyncData
        /// </summary>
        /// <param name="data"></param>
        public void AplyChangesToObjectsInternal(SyncData data)
        {
            int id = data.oid;
            NetObjects refObj = RefObjects.Find(s => s.ID == id);

            if (refObj.Obj != null)
            {
                int i = 0;
                foreach (var item in data.PropertiesValues)
                {
                    string propName = data.PropertiesNames[i];
                    i++;

                    refObj.Obj.GetType().GetProperty(propName).SetValue(refObj.Obj, Convert.ChangeType(item, refObj.Obj.GetType().GetProperty(propName).PropertyType), null);
                    
                }

                NetObjects basObj = BaseObjects.Find(s => s.ID == id);
                basObj.Obj = ((ISyncable)refObj.Obj).Copy();
            }
            
        }
        #endregion

    }
}
